package com.spzx.product.service.impl;

import com.spzx.product.service.TestService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class TestServiceImpl implements TestService {

   private static final Logger log = LoggerFactory.getLogger(TestServiceImpl.class);
   @Autowired
   private StringRedisTemplate redisTemplate;



/*   //集群的情况下加同步锁无法保证数据的原子性,会造成数据的不完整,所以我们要加分布锁
   @Override
   public synchronized void testLock() {
      // 查询Redis中的num值
      String value = this.stringRedisTemplate.opsForValue().get("num");
      // 没有该值return
      if (StringUtils.isBlank(value)){
         return ;
      }
      // 有值就转成int
      int num = Integer.parseInt(value);
      // 把Redis中的num值+1
      this.stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
   }*/

   @Override
   public void testLock() {
      String lockName="lock";
     // String lockValue="lock";
/*      //版本1 获取redis分布式锁 采用Redis提供setnx命令 存在问题：A线程加锁成功后，逻辑执行中服务宕机，锁无法释放造成死锁
                                 //setIfAbsent不存在才创建相当于setnx
      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockname,lockvalue);*/

/*      // 版本2：在版本1基础上，通过expire命令为锁的Key设置过期时间，防止死锁 问题：加锁跟设置锁过期时间不具备原子性可能在加锁后没来得及设置过期时间服务器宕机了导致死锁
      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue);
      //防止死锁，为锁的Key设置过期时间
      redisTemplate.expire(lockName,5, TimeUnit.SECONDS);*/

/*      // 版本3: 使用set命令配合 NX EX 参数 替换版本2的命令 确保 写入值跟设置过期时间原子性 问题：当线程1超时后redis自动释放锁,这时线程2抢到锁了,开始执行业务逻辑,线程1这时释放锁把线程2的锁释放了
      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);*/

      //版本4：在版本3基础上，存入Redis锁的值，为每个线程生成UUID作为线程标识。
      String lockValue = UUID.randomUUID().toString();
      Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockName, lockValue, 5, TimeUnit.SECONDS);

      //获取分布式锁后执行业务逻辑
      if(flag){
         try {
            String value = this.redisTemplate.opsForValue().get("num");
            // 没有该值return
            if (StringUtils.isBlank(value)){
               return ;
            }
            // 有值就转成int
            int num = Integer.parseInt(value);
            // 把Redis中的num值+1
            this.redisTemplate.opsForValue().set("num", String.valueOf(++num));
         } finally {
/*            //版本4：先判断Redis中锁的值跟当前线程虚拟机栈获取当前线程标识值是否一致，一致则删除锁  问题：判断跟删除锁不是原子操作判断是线程1的锁蛋这是锁被redis释放了

            if (redisTemplate.opsForValue().get(lockName).equals(lockValue)) {
               redisTemplate.delete(lockName);
            }*/

            //5.版本5：使用lua脚本删除锁，确保删除锁跟判断锁值一致性
            // 问题：1.编码麻烦  2.底层采用String无法锁可重入  3.加锁后没有锁自动续期机制(加锁后线程1执行业务超时redis删除锁,这时锁被线程2抢到开始执行业务,就会有两个线程都在执行业务)  版本6：采用生产框架Redisson提供分布式锁
            //5.1 创建Redis脚本对象，封装lua脚本
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end\n";
            RedisScript<Boolean> booleanRedisScript = new DefaultRedisScript<>(script, Boolean.class);
            redisTemplate.execute(booleanRedisScript, Arrays.asList(lockName), lockValue);
         }
      }else {
         try {
            System.out.println(Thread.currentThread().getName()+"抢锁失败再次抢锁");
            Thread.sleep(50);
         } catch (InterruptedException e) {
            throw new RuntimeException(e);
         }
         this.testLock();
      }
   }




}